package ru.osu.acm.system;

import ru.osu.acm.common.config.Compiler;
import ru.osu.acm.common.config.Config;
import ru.osu.acm.common.entities.Checker;
import ru.osu.acm.common.entities.Problem;
import ru.osu.acm.common.entities.Submission;
import ru.osu.acm.common.industrial.ProblemFactory;
import ru.osu.acm.common.industrial.CheckerFactory;
import ru.osu.acm.common.industrial.TestcaseFactory;
import ru.osu.acm.testing.Utils;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.PrintWriter;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Date;

/**
 * User: Aleksey
 * Date: 17.02.2009
 * Time: 16:34:41
 */
public class Tester {
    private static int COMPILE_SUCCESSFULL = 0;
    private static int COMPILE_ERROR = 1;

    public static int STATUS_RECEIVED = 0;
    public static int STATUS_QUEUED = 1;
    public static int STATUS_COMPILING = 2;
    public static int STATUS_RUNNING = 3;
    public static int STATUS_ACCEPTED = 4;
    public static int STATUS_COMPILE_ERROR = 5;
    public static int STATUS_RUNTIME_ERROR = 6; // TODO:
    public static int STATUS_WRONG_ANSWER = 7;
    public static int STATUS_TIME_LIMIT_EXCEEDED = 8;
    public static int STATUS_MEMORY_LIMIT_EXCEEDED = 9; // TODO:
    public static int STATUS_OUTPUT_LIMIT_EXCEEDED = 10;
    public static int STATUS_CHECK_FAILED = 11; // TODO:
    public static int STATUS_PROBLEM_NOT_FOUND = 12;
    public static int STATUS_NO_OUTPUT_FILE = 13;
    public static int STATUS_DUMMY = 14;

    public static int MAX_OUTPUT_FILE_SIZE = 1000000;

    private Connection con;

    private void connectToDatabase() throws SQLException {
        String database = Config.database;
        String username = Config.username;
        String password = Config.password;

        try {
            Class.forName(Config.driver);
        }
        catch (ClassNotFoundException e) {
            Logger.logError(e);
        }
        con = DriverManager.getConnection(Config.uri + database, username, password);
    }

    private void disconnectFromDatabase() throws SQLException {
        con.close();
    }

    private void updateStatus(Submission submission, int status) throws SQLException {
        PreparedStatement statement = con.prepareStatement("update submission set status = ? where id = ?");
        statement.setInt(1, status);
        statement.setInt(2, submission.getId());
        statement.executeUpdate();
        statement.close();
    }

    private void updateStatus(Submission submission, TestResult result) throws SQLException {
        PreparedStatement statement = con.prepareStatement("update submission set status = ?, testtime = ?, test = ?  where id = ?");
        statement.setInt(1, result.status);
        statement.setLong(2, result.time);
        statement.setInt(3, (result.test + 1));
        statement.setInt(4, submission.getId());
        statement.executeUpdate();
        statement.close();
    }

    private void saveTextFile(String file, String source) {
        try {
            PrintWriter pw = new PrintWriter(file);
            pw.write(source);
            pw.close();
        }
        catch (FileNotFoundException e) {
            e.printStackTrace();
        }
    }

    private int compileSubmission(Submission submission, String workDir) {
        Compiler compiler = Config.compilers.get(submission.getCompiler());

        new File(workDir).mkdirs();
        String fullFilePath = workDir + File.separatorChar + submission.getFilename() + "." + compiler.getExtension();

        saveTextFile(fullFilePath, submission.getSource());

        String compileString = compiler.getCompileString();
        compileString = compileString.replace("%FULLFILEPATH%", fullFilePath);
        compileString = compileString.replace("%FULLFILEDIR%", workDir);
        compileString = compileString.replace("%FILENAME%", submission.getFilename());

        try {
            Logger.log(compileString);
            Process p = Runtime.getRuntime().exec(compileString, null, new File(workDir));

            new Thread(new ProcessStreamReader(p.getInputStream())).start();
            new Thread(new ProcessStreamReader(p.getErrorStream())).start();

            try {
                p.waitFor();
            }
            catch (InterruptedException e) {
                Logger.logError(e);
            }
            int returnValue = p.exitValue();
            if (returnValue == 0) {
                return COMPILE_SUCCESSFULL;
            }
            return COMPILE_ERROR;
        }
        catch (IOException e) {
            Logger.logError(e);
        }
        return COMPILE_ERROR;
    }

    private class TestResult {
        int status;
        int test;
        long time;

        private TestResult(int status, int test, long time) {
            this.status = status;
            this.test = test;
            this.time = time;
        }

        private TestResult(int status) {
            this.status = status;
            test = 0;
            time = 0;
        }
    }

    private void deleteNonLegalFiles(String dir, Compiler compiler, Submission submission) {
        for (File file : new File(dir).listFiles()) {
            Boolean legal = false;
            for (int i = 0; i < compiler.getLegalFiles().size(); i++) {
                String legalFile = compiler.getLegalFiles().get(i);
                legalFile = legalFile.replace("%FILENAME%", submission.getFilename());
                if (file.getName().toLowerCase().equals(legalFile.toLowerCase())) {
                    legal = true;
                    break;
                }
            }
            if (file.getName().toLowerCase().endsWith(".class")) { // TODO: refactor costyl // TODO: is this really not good ? checkers ?
                legal = true;
            }
            if (file.isDirectory() && file.getName().toLowerCase().equals("checker")) {
                legal = true;
            }
            if (!legal) {
                if (file.delete()) { // TODO: clear directories!!!
                    Logger.log("    deleted file: " + file.getAbsolutePath());
                }
            }
        }
    }

    private void compileChecker(String checkerDir, Checker checker) {
        if (new File(checkerDir).mkdirs()) {
            String checkerSourcePath = checkerDir + File.separatorChar + checker.getFilename() + ".java"; // TODO: checkers only on java ?
            saveTextFile(checkerSourcePath, checker.getSource());
            String compileString = Config.checkerCompileString.replace("%FULLFILEPATH%", checkerSourcePath);
            try {
                Logger.log("Compiling checker: " + compileString);
                Process p = Runtime.getRuntime().exec(compileString, null, new File(checkerDir));

                new Thread(new ProcessStreamReader(p.getInputStream())).start();
                new Thread(new ProcessStreamReader(p.getErrorStream())).start();

                try {
                    p.waitFor();
                }
                catch (InterruptedException e) {
                    Logger.logError(e);
                }
            }
            catch (IOException e) {
                Logger.logError(e);
            }
        }
    }

    private void clearTempDir(String workDir, String checkerDir) { // TODO: do recursively for illegal files, man
        Utils.clearDir(new File(workDir));
    }

    private TestResult testSubmission(Submission submission, String workDir, String checkerDir, Checker checker) {
        Problem problem = ProblemFactory.getProblem(submission.getProblem().getId(), con, false);

        if (problem == null) {
            return new TestResult(STATUS_PROBLEM_NOT_FOUND);
        }

        ru.osu.acm.common.config.Compiler compiler = Config.compilers.get(submission.getCompiler());

        int inputCount = TestcaseFactory.getTestCountForProblem(problem.getId(), con, false);

        String executeString = compiler.getExecuteString();
        executeString = executeString.replace("%FULLFILEDIR%", workDir);
        executeString = executeString.replace("%FILENAME%", submission.getFilename());


        for (int i = 0; i < inputCount; i++) {
            String inputFileTargetPath = workDir + File.separatorChar + problem.getInputFile();
            saveTextFile(inputFileTargetPath, TestcaseFactory.getInputTestcase(problem.getId(), i, con, false));

            Boolean tle = false;
            java.util.Date dateBefore = new Date(), dateAfter = new Date();
            try {
                dateBefore = new java.util.Date(new Date().getTime() + 200);

                Logger.log(executeString);
                Process p = Runtime.getRuntime().exec(executeString, null, new File(workDir));

                new Thread(new ProcessStreamReader(p.getInputStream())).start();
                new Thread(new ProcessStreamReader(p.getErrorStream())).start();

                dateAfter = new Date();
                while (true) {
                    try {
                        Thread.sleep(200);
                    }
                    catch (InterruptedException e) {
                        Logger.logError(e);
                    }
                    java.util.Date curDateAfter = new java.util.Date();
                    if (curDateAfter.getTime() - dateBefore.getTime() > problem.getTimeLimit(i) * 1000) {
                        try {
                            p.exitValue();
                        }
                        catch (IllegalThreadStateException e) {
                            tle = true;
                            dateAfter = curDateAfter;
                            p.destroy();
                        }
                        break;
                    }
                    else {
                        dateAfter = curDateAfter;

                        boolean exited = true;
                        try {
                            p.exitValue();
                        }
                        catch (IllegalThreadStateException e) {
                            exited = false;
                        }
                        if (exited) {
                            break;
                        }
                    }
                }

                try {
                    p.waitFor();
                }
                catch (InterruptedException e) {
                    Logger.logError(e);
                }

            }
            catch (IOException e) {
                Logger.logError(e);
            }
            if (tle) {
                deleteNonLegalFiles(workDir, compiler, submission);
                Logger.log(" * Test " + i + ": Time Limit Exceeded " + (0.001 * (dateAfter.getTime() - dateBefore.getTime())) + " sec.");
                return new TestResult(STATUS_TIME_LIMIT_EXCEEDED, i, dateAfter.getTime() - dateBefore.getTime());
            }

            String checkerString = Config.checkerExecuteString;

            String templateInputPath = checkerDir + File.separatorChar + "templateinput.txt";
            String templateOutputPath = checkerDir + File.separatorChar + "templateoutput.txt";

            saveTextFile(templateInputPath, TestcaseFactory.getInputTestcase(problem.getId(), i, con, false));
            saveTextFile(templateOutputPath, TestcaseFactory.getOutputTestcase(problem.getId(), i, con, false));

            checkerString = checkerString.replace("%FILENAME%", checker.getFilename());
            checkerString = checkerString.replace("%INPUTFILE%", templateInputPath);
            checkerString = checkerString.replace("%USEROUTPUTFILE%", workDir + File.separatorChar + problem.getOutputFile());
            checkerString = checkerString.replace("%TEMPLATEOUTPUTFILE%", templateOutputPath);

            if (!new File(workDir + File.separatorChar + problem.getOutputFile()).exists()) {
                deleteNonLegalFiles(workDir, compiler, submission);
                Logger.log(" * Test " + i + ": No Output File");
                return new TestResult(STATUS_NO_OUTPUT_FILE, i, dateAfter.getTime() - dateBefore.getTime());
            }
            if (new File(workDir + File.separatorChar + problem.getOutputFile()).length() > MAX_OUTPUT_FILE_SIZE) {
                deleteNonLegalFiles(workDir, compiler, submission);
                Logger.log(" * Test " + i + ": Output Limit Exceeded");
                return new TestResult(STATUS_OUTPUT_LIMIT_EXCEEDED, i, dateAfter.getTime() - dateBefore.getTime());
            }

            Process p = null;
            try {
                p = Runtime.getRuntime().exec(checkerString, null, new File(checkerDir));

                new Thread(new ProcessStreamReader(p.getInputStream())).start();
                new Thread(new ProcessStreamReader(p.getErrorStream())).start();

                p.waitFor();
            }
            catch (IOException e) {
                Logger.logError(e);
            }
            catch (InterruptedException e) {
                Logger.logError(e);
            }
            deleteNonLegalFiles(workDir, compiler, submission);
            if (p.exitValue() != 0) {
                Logger.log(" * Test " + i + ": Wrong Answer");
                return new TestResult(STATUS_WRONG_ANSWER, i, dateAfter.getTime() - dateBefore.getTime());
            }
            Logger.log(" * Test " + i + ": OK, " + (0.001 * (dateAfter.getTime() - dateBefore.getTime())) + " sec.");
        }

        Logger.log(" * Accepted");
        return new TestResult(STATUS_ACCEPTED, inputCount - 1, 0);
    }

    public void test(ArrayList<Submission> submissions) {
        Logger.log("Enetered tester");
        try {
            Logger.log("Connecting to database");
            connectToDatabase();
            try {
                for (Submission submission : submissions) {
                    updateStatus(submission, STATUS_QUEUED);
                }
                for (Submission submission : submissions) {
                    updateStatus(submission, STATUS_COMPILING);

                    String workDir = System.getProperty("java.io.tmpdir") + File.separatorChar + "olga";

                    Logger.log("Compiling submission " + submission.getId());
                    if (submission.getCompiler().equals("py") || compileSubmission(submission, workDir) == COMPILE_SUCCESSFULL) {
                        updateStatus(submission, STATUS_RUNNING);
                        Logger.log("Compile successfull. Running submission " + submission.getId());

                        Checker checker = submission.getProblem().getChecker();
                        String checkerDir = workDir + File.separatorChar + "checker";
                        compileChecker(checkerDir, checker);

                        TestResult testResult = testSubmission(submission, workDir, checkerDir, checker);

                        clearTempDir(workDir, checkerDir);

                        updateStatus(submission, testResult);
                        Logger.log("Submission " + submission.getId() + ": test result = " + testResult.status + ", test " + testResult.test);
                    }
                    else {
                        Logger.log("Compile error");
                        updateStatus(submission, STATUS_COMPILE_ERROR);
                    }
                }
            }
            finally {
                disconnectFromDatabase();
            }
        }
        catch (SQLException e) {
            Logger.logError(e);
        }
    }
}
